﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace Miado.Mapping
{
    /// <summary>
    /// This class maps properties on business objects 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public abstract class RowMapper<T>
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="RowMapper&lt;T&gt;"/> class.
        /// </summary>
        protected RowMapper() 
        {
            ColumnMappings = new Dictionary<string, PropertyInfo>();
        }

        /// <summary>
        /// Gets or sets the mappings of column names to Properties 
        /// on the underlying business object.
        /// </summary>
        /// <value>The mappings of column names to Properties.</value>
        public IDictionary<string, PropertyInfo> ColumnMappings { get; private set; }

        /// <summary>
        /// This method maps a property on a business object to a 
        /// column in a result set row.
        /// </summary>
        /// <param name="expression">The expression representing a 
        /// Property call.</param>
        /// <param name="columnName">The name of the database column.</param>
        /// <returns>A reference to this same object so that the method 
        /// can be chained.</returns>
        public RowMapper<T> Map(Expression<Func<T, object>> expression, string columnName)
        {
            if ( String.IsNullOrEmpty(columnName) )
            {
                throw new ArgumentNullException("columnName");
            }
            ColumnMappings[columnName] = GetPropertyInfo(expression);
            return this;
        }

        /// <summary>
        /// This method maps a property on a business object to a 
        /// column identically named in a result set row.
        /// </summary>
        /// <param name="expression">The expression representing a 
        /// Property call.</param>
        /// <returns>A reference to this same object so that the method 
        /// can be chained.</returns>
        public RowMapper<T> Map(Expression<Func<T, object>> expression)
        {
            PropertyInfo property = GetPropertyInfo(expression);
            ColumnMappings[property.Name] = property;
            return this;
        }

        /// <summary>
        /// Creates the entity from a result set row using the mappings 
        /// found in this mapping object.
        /// </summary>
        /// <param name="row">The IResultSetRow object, which is passed
        /// in from the results of a query exection in Miado.</param>
        /// <returns>A populated instance of an Entity.</returns>
        public virtual T CreateEntity(IResultSetRow row)
        {
            T entity = Activator.CreateInstance<T>();

            foreach ( var columnName in ColumnMappings.Keys )
            {
                if ( !row.IsDBNull(columnName) )
                {
                    ColumnMappings[columnName].SetValue(entity, row.GetValue(columnName), null);
                }
            }

            return entity;
        }

        /// <summary>
        /// Gets the PropertyInfo reflection object from the lambda expression passed into 
        /// the mapping.
        /// </summary>
        /// <typeparam name="TProperty">The Property on the object.</typeparam>
        /// <typeparam name="TValue">The value that would be returned.</typeparam>
        /// <param name="expression">The lambda expression that represents a Property
        /// on the entity.</param>
        /// <returns>the PropertyInfo reflection object.</returns>
        private static PropertyInfo GetPropertyInfo<TProperty, TValue>(Expression<Func<TProperty, TValue>> expression)
        {
            return GetMemberExpression(expression).Member as PropertyInfo;
        }

        /// <summary>
        /// Gets the MemberExpression parsed from the lambda expression reprensenting 
        /// a Property on the entity.
        /// </summary>
        /// <typeparam name="TProperty">The Property on the object.</typeparam>
        /// <typeparam name="TValue">The value that would be returned.</typeparam>
        /// <param name="expression">The lambda expression that represents a Property
        /// on the entity.</param>
        /// <returns>the MembershipExpression object.</returns>
        private static MemberExpression GetMemberExpression<TProperty, TValue>(Expression<Func<TProperty, TValue>> expression)
        {
            MemberExpression memberExpression;
            if ( expression.Body.NodeType == ExpressionType.Convert )
            {
                var body = (UnaryExpression)expression.Body;
                memberExpression = body.Operand as MemberExpression;
            }
            else if ( expression.Body.NodeType == ExpressionType.MemberAccess )
            {
                memberExpression = expression.Body as MemberExpression;
            }
            else
            {
                throw new ArgumentException("Expression does not represent a valid Property on an object", "expression");
            }

            return memberExpression;
        }

    }
}
